﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Xml;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace TowerDefence
{
    public static class LayerNames
    {
        public const string TERRAIN = "terrain";
        public const string LOGICAL = "logical";
        public const string AIPATH1 = "aipath1";
        public const string AIPATH2 = "aipath2";
        public const string AIPATH3 = "aipath3";
        public const string AIPATH4 = "aipath4";
    }

    public class Layer
    {
        internal struct TileInfo
        {
            public Texture2D Texture;
            public Rectangle Rectangle;
        }

        public string Name;
        public int Width, Height;
        public int[] Tiles;
        
        private float? opacity;
        private Color color;
        private bool visible = true;
        
        internal TileInfo[] _TileInfoCache = null;

        internal static Layer Load(XmlReader reader)
        {
            var result = new Layer();

            result.Name = reader.GetAttribute("name");
            result.Width = int.Parse(reader.GetAttribute("width"));
            result.Height = int.Parse(reader.GetAttribute("height"));
            result.Tiles = new int[result.Width * result.Height];
            if (reader.GetAttribute("opacity") != null) {
                result.opacity = float.Parse(reader.GetAttribute("opacity"));
            }
            // i assume if this attribute is defined at all, it means the layer is invisible (value of "0")
            if (reader.GetAttribute("visible") != null) { 
                result.visible = false; 
            }

            while (!reader.EOF)
            {
                var name = reader.Name;

                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        if (name == "data")
                        {
                            var encoding = reader.GetAttribute("encoding");
                            var compressor = reader.GetAttribute("compression");
                            switch (encoding)
                            {
                                case "base64":
                                    {
                                        int dataSize = (result.Width * result.Height * 4) + 1024;
                                        var buffer = new byte[dataSize];
                                        reader.ReadElementContentAsBase64(buffer, 0, dataSize);

                                        Stream stream = new MemoryStream(buffer, false);
                                        if (compressor == "gzip")
                                            stream = new GZipStream(stream, CompressionMode.Decompress, false);

                                        using (stream)
                                        using (var br = new BinaryReader(stream))
                                        {
                                            for (int i = 0; i < result.Tiles.Length; i++)
                                                result.Tiles[i] = br.ReadInt32();
                                        }

                                        continue;
                                    };

                                default:
                                    throw new Exception("Unrecognized encoding.");
                            }
                        }

                        break;
                    case XmlNodeType.EndElement:
                        break;
                }

                reader.Read();
            }

            return result;
        }

        public int GetTile(int x, int y)
        {
            if ((x < 0) || (y < 0) || (x >= Width) || (y >= Height))
                throw new InvalidOperationException();

            int index = (y * Width) + x;
            return Tiles[index];
        }

        protected void BuildTileInfoCache(List<Tileset> tilesets)
        {            
            // we might not have an opacity for this layer
            byte alpha = (byte)(255 * this.opacity ?? 255);
            this.color = new Color(Color.White, alpha);
            Rectangle rect = new Rectangle();
            var cache = new List<TileInfo>();
            int i = 1;

        next:
            for (int t = 0; t < tilesets.Count; t++)
            {
                if (tilesets[t].MapTileToRect(i, ref rect))
                {
                    cache.Add(new TileInfo
                    {
                        Texture = tilesets[t].Texture,
                        Rectangle = rect
                    });
                    i += 1;
                    goto next;
                }
            }

            _TileInfoCache = cache.ToArray();
        }

        public void Draw(SpriteBatch batch, List<Tileset> tilesets, Rectangle rectangle, Vector2 viewportPosition, int tileWidth, int tileHeight)
        {
            if (!this.visible) return;            

            int i = 0;
            Vector2 destPos = new Vector2(rectangle.Left, rectangle.Top);
            Vector2 viewPos = viewportPosition;

            int minX = (int)Math.Floor(viewportPosition.X / tileWidth);
            int minY = (int)Math.Floor(viewportPosition.Y / tileHeight);
            int maxX = (int)Math.Ceiling((rectangle.Width + viewportPosition.X) / tileWidth);
            int maxY = (int)Math.Ceiling((rectangle.Height + viewportPosition.Y) / tileHeight);

            if (minX < 0)
                minX = 0;
            if (minY < 0)
                minY = 0;
            if (maxX >= Width)
                maxX = Width - 1;
            if (maxY >= Height)
                maxY = Height - 1;

            if (viewPos.X > 0)
            {
                viewPos.X = ((int)Math.Floor(viewPos.X)) % tileWidth;
            }
            else
            {
                viewPos.X = (float)Math.Floor(viewPos.X);
            }

            if (viewPos.Y > 0)
            {
                viewPos.Y = ((int)Math.Floor(viewPos.Y)) % tileHeight;
            }
            else
            {
                viewPos.Y = (float)Math.Floor(viewPos.Y);
            }

            TileInfo info = new TileInfo();
            if (_TileInfoCache == null)
                BuildTileInfoCache(tilesets);

            for (int y = minY; y <= maxY; y++)
            {
                destPos.X = rectangle.Left;

                for (int x = minX; x <= maxX; x++)
                {
                    i = (y * Width) + x;
                    int index = Tiles[i] - 1;

                    if ((index >= 0) && (index < _TileInfoCache.Length))
                    {
                        info = _TileInfoCache[index];
                        batch.Draw(info.Texture, destPos - viewPos, info.Rectangle, this.color);
                    }

                    destPos.X += tileWidth;
                }

                destPos.Y += tileHeight;
            }
        }
    }
}
